Who? I am Michael Albaugh (formerly WB6SYQ, but I let that lapse and it was apparently reused for another radio amateur). I am a volunteer docent at the Computer History Museum. Among the associated benefits is the opportunity to work with the restoration crew for a pair of IBM 1401 computers, and to demonstrate those computers to the public. Paul Laughton (AC6B) is also a docent and 1401 demonstrator, and suggested a special demo to be given to the Palo Alto Amateur Radio Association when they visited.

What? The demo involved the IBM 1401 computer sending a greeting in Morse Code, by manipulating the RFI produced by its operation. It sounded like this.

You could also watch the video versions of the Demo and Intro

When? March 21, 2015.

Where? Mountain View California.

Why? For fun, and to demonstrate why things like TEMPEST exist. Paul knew that I was an ex-ham and had been messing around trying to implement an RF-based music program such as had been popular on the 1401 during its heyday. This seemed like a logical offshoot.

How? The 1401 has a memory cycle time of about 11 microseconds, so runs at about 90kHz. The seventh harmonic of that is about 630kHz, which is near the bottom of the AM broadcast band. A MLC (Move Left Characters) instruction uses 2 memory cycles per character moved, and produces a fairly steady pattern. A loop repeating such a move will produce a glitch corresponding to the much shorter bursts of memory access for instructions and counters to implement the loop. Those glitches will produce an audible tone. Similarly, a very long move, even in a loop, will produce a low frequency, possibly inaudible, at least in a noisy machine room.

Following is the code that produced that demo. The structure of this code is influenced by my desire to re-use cards from the object decks of preceding versions, since I was assembling each version then hand-punching the object code resulting from the assembly. Because of that, I occasionally did "patches", where I changed the source code such that only a small number of characters in a given object card would change. Using the DUPlicate function of the keypunch, I was able to change only those characters. Of course the new data had to be the same length as the old. I'm using this as a general purpose excuse for anything weird and otherwise inexplicable.

               JOB  MORSE HELLO VIA RF FROM 1401
     *   111111111122222222223333333333444444444455555555556666666666777
     * 89012345678901234567890123456789012345678901234567890123456789012
     * $Revision: 1.7 $
     *
     * ASSEMBLE WITH COMMAND
     * AUTOCODER -L MHELLO.LIS -O MHELLO.CD -BV MHELLO.AC
               CTL  6111
Above is pretty much boilerplate, and a bit anachronistic. The two lines of numbers are something I include as a visual aid when typing assembly code, as both supported assemblers (SPS and AUTOCODER) have formatting requirements1. In real life, the column numbers would be printed on the coding sheets and I could look at the column indicator of the keypunch.

First off, we define some useful symbols. The Index Registers of the 1401 are not actually registers, but dedicated storage locations tucked at the top of the first 100 characters of memory. The AUTOCODER assembler recognizes the sybols X1, X2, and X3 as indicating indexing in particular contexts, but if you refer to these locations in another context, the symbols are not defined. You could call them anything, but sanity tends to prevail.

     * INDEX REGISTERS
               ORG  87
     X1        DS   3
               ORG  92
     X2        DS   3
               ORG  97
     X3        DS   3
You can probably figure out what ORG is. DS is "Define Symbol", which defines a symbol to have the value of the current location, and reserves some memory. EQU is similar, but typically for constants, reserving no memory, and can be a (limited form) expression.

The next two lines refer to the implicit use of some memory2

Comments start two spaces after the last character that could be a legal part of an instruction or pseudo-op. Whole-line comments start with an asterisk in column one, but you figured that out.

     * MISC DATA IN PUNCH BUFF. NOT PUNCHING
               ORG  100
     MSG       EQU  99     COMPENSATE FOR PRE-INCREMENT
     MSGNX     EQU  MSG&1  FOR SPACE LOOKAHEAD
Note the '&' above. If I were using a Scientific print-chain, that would render as a '+'. Similarly, the '@' characters below would be apostrophes.3
     * MESSAGE LIVES AT 100. MUST FIT BEFORE CODE AT 400
Each characters is represented by a DC (Define Constant) of a string enclosed in '@' symbols, followed by a comment with its corresponding character, for those innocent of Morse. The resulting string of '*', '-', and ' ' is read by the code at CLOOP, which generates the appropriate sound (or silence).
     * CURRENTLY HELLO DE 1401 QSL?
               DC   @**** @   H
               DC   @* @      E
               DC   @*-** @   L
               DC   @*-** @   L
               DC   @--- @    O
               DC   @ @
               DC   @-** @    D
               DC   @* @      E
               DC   @ @
               DC   @** @     I
               DC   @-*** @   B
               DC   @-- @     M
               DC   @*---- @  1
               DC   @****- @  4
               DC   @----- @  0
               DC   @*---- @  1
               DC   @ @
               DC   @--*- @   Q
               DC   @*** @    S
               DC   @*-** @   L
               DC   @**--* @  ?
Note that the line above contains a typo. It was spotted by a reviewer after I had run the code and made the recording, so I have left it as is until I can fix it.

As the below says, Paul requested that the message start with two 'V' characters, to make it easier to "Sync Up" with the sending rate. Since the message loops, the end is as good a place as the beginning, and less extra punching. The code that reads the message loops back to the beginning if it sees any character other than '*', '-', or space.

               DC   @    @    TWO WORD SPACES BEFORE LOOPING
               DC   @***- ***-  @  VV ADDED PER PAUL
               DC   @X@
Here's the basic principle. The thing about the existing music programs is that they don't do rests. You can change the pitch, but you can't get them to shut up. That's not good for Morse Code, so I tried a variety of methods to make the desired sound (and lack thereof). You can see that experiment in mmtest.html

The choice of a very long move for silence limits me to 1401s with at least 12000 characters of memory.4

     * BASIC TIMING IS BY LONG MOVES, OF MEMORY BASED AT MBASE
     MBASE     EQU  1000          MOVE BASE
     TADDR     EQU  MBASE&54      54 CHAR MOVE FOR TONE
     SADDR     EQU  MBASE&10435   10435 CHAR MOVE FOR SILENCE
     *
               ORG  400
     * THE MESSAGE IS BAKED INTO THIS PROGRAM AT MSG
     * ENCODED AS * FOR DIT, - FOR DAH, SPACE BETWEEN CHARACTERS.
     * TWO SPACES FOR WORD-SPACE.
You could chose to clear all of memory before loading code, or not. WM stands for Word Mark. SBR (Store B Register) is a sort of Swiss Army Knife Instruction. In this case, it is used to initialize the X1 index register to the effective address in the second operand. Zero is a boring form of that.
     * SETUP ASSUMES CLEAR-STORAGE BOOTSTRAP, SETS WM
     * FOR OVERLAPPING AREAS TO BE MOVED TO SELF FOR
     * TONE/SILENCE
     SETUP     SW   MBASE
               SBR  X1,0
The major loop. MA (Modify Address) is a special form of addition that operates on the strange form of addresses on systems with more than 4K.5 BCE is Branch Character Equal. You can probably figure out the rest, although I should point out that the first operand of an instruction is the Source, and the second the Destination. Usually.

There are no Immediates, so XINC has to be a constant in memory.6

     *
     * CLOOP: LOOP ACROSS ALL CHARS OF MESSAGE, UNTIL WE
     * SEE A CHARACTER WHICH IS NOT *, -, OR SPACE.
     * RESET POINTER (IN X1) AT END, TO LOOP MESSAGE.
     * START BY INCREMENTING POINTER. DEFINITION OF
     * MSG IS TWEAKED TO MAKE THIS WORK, AND AVOID WRAP
     CLOOP     MA   XINC,X1
               BCE  DIT,MSG&X1,*
               BCE  DAH,MSG&X1,-
               BCE  SPC,MSG&X1, 
     * IF NOT AN EXPECTED CHARACTER, RESET POINTER TO 1ST CHAR
               SBR  X1, 0
               B    CLOOP       NO PAUSE BEFORE RESTART
CLOOP dispatches to one of the three routines below. We decided on 5 WPM because it is no longer appropriate to assume that all hams can copy Morse faster, or even at all.7
     * ROUTINES FOR '*', '-', ' '
     * DIT IS ONE ELEMENT OF TONE, GENERATED BY MID-LENGTH
     * MOVE (MLC) PER STAN PADDOCK DESCRIPTION OF MUSIC CODE
     *
     * WITH A TARGET SPEED OF 5 WPM, A DIT IS 240 MILLISECONDS
     * IF OUR BASE TONE IS ABOUT 800HZ, A CYCLE IS ABOUT 1.25
     * MILLISECONDS, SO A DIT IS A BIT LESS THAN 200 CYCLES.
     * A DAH IS 3 TIMES AS LONG. BOTH JUST SET ELTIM AND MERGE.
Note that loop counts are done by adding until a counter overflows. The BAV (Branch on Arithmetic Overflow) test is the shortest/fastest way to check for loop termination.8 We want to minimize time spent doing anything but the Move instruction, to keep the output as clean as possible.
     DIT       MLC  DITCT,ELTIM   OVERFLOW AFTER ADDING 1 ELT WORTH
               B    TONE
     DAH       MLC  DAHCT,ELTIM   OVERFLOW AFTER ADDING 3 ELT WORTH
     *
     * TONE IS GENERATED BY A MOVE OF XXX CHARACTERS. EACH
     * CHARACTER MOVED TAKES TWO MEMORY CYCLES, 23 MICROSECONDS
     * SO A MOVE OF 108-ISH CHARACTERS WILL DO. SOME OVERHEAD
     * FOR THE INSTRUCTIONS THEMSELVES WILL LOWER THE TONE, OR
     * WE CAN TRIM THE MOVE.
     TONE      MLC  TADDR,TADDR
               A    ELINC,ELTIM
               BAV  ESPC       ON OVERFLOW, DO 1 ELEMENT SPACING
               B    TONE
     *
     * VARIOUS LENGTHS OF SILENCE BELOW, THEN BACK TO CLOOP
     * 240 MILLISECONDS IS 240000 MICROSECONDS, OR ABOUT 20870
     * CYCLES. SO A MOVE OF 10435 CHARACTERS IS ABOUT RIGHT
     * THERE MAY BE A SLIGHT CLICK AT MOVE BOUNDARIES,
That slight click does exist, and could be really annoying if I wanted to run on a small machine, using more moves of shorter length.
     *
     * WE GET TO SPC IF MSG CHARACTER IS SPACE. A SINGLE SPACE
     * IS AN INTER-CHARACTER SPACE. SO WE NEED 3 ELEMENTS OF
     * SILENCE, IF IT IS FOLLOWED BY A SPACE, WE NEED SEVEN
     * TOTAL FOR A WORD SPACE.
     * WE HAVE ALREADY DONE ONE VIA ESPC AFTER PREVIOUS ELEMENT,
     * SO DO TWO OR SIX MORE,
     SPC       B    WSPC,MSGNX&X1,  6 MORE IF NEXT CHAR IS SPACE
               B    CSPC            2 MORE FOR CHARACTER SPACE
     WSPC      MLC  SADDR,SADDR     WORD SPACE
               MLC  SADDR,SADDR
               MLC  SADDR,SADDR
               MLC  SADDR,SADDR
     CSPC      MLC  SADDR,SADDR     CHARACTER SPACE
     ESPC      MLC  SADDR,SADDR     ELEMENT SPACE
               B    CLOOP
All over but the variables. Well, only ELTIM is an uninitialized variable. (the #3 says to just reserve three characters) The rest are initialized variables that are used as constants.
     ELTIM     DCW  #3
     XINC      DCW  @001@
     DITCT     DCW  @800@          OVERFLOW AFTER 200 (800HZ)
     DAHCT     DCW  @400@          OVERFLOW AFTER 600 (800HZ)
     ELINC     DCW  @1@            1 for 5WPM, 2 FOR 10WPM
               END  SETUP

OBJECT DECK

Code can be loaded into the 1401 by resetting and pressing the LOAD button. This causes a card to be read into locations 1-80, wordmarks to be cleared from those locations, a wordmark to be set at location 1, and control to be transfered to location 1.

You can see a clear division about midway through the card images below. Essentially, the left hand side contains data and instructions to be loaded, while the right had side contains the instructions to move the left side into the appropriate address and set any needed wordmarks. This is not strictly true of the first two cards, which do a bit of housekeeping like clearing memory.

You may also notice that the last four columns contain a sequence number, useful when you drop the deck, although unless you are doing stuff like overlays, getting the first two and the last one card in the right order is often sufficient.

,008015,022026,030040/019,001L020100   ,047054,061068,072072)08108110220001     
,008047/047046       /000H025B022100  4/061046,054061,068072,00104010400002     
**** * *-** *-** ---  -** *  ** -***   L037136)100100,040040,04004010400003     
-- *---- ****- ----- *----  --*- ***   L037173)137137,040040,04004010400004     
*-** **--*     ***- ***-  X            L027200)174174,040040,04004010400005     
,|00H089000#557089B4530Z9*B4640Z9-     L034433,404411,418426,04004010400006     
B4940Z9 H089000B411M560554B471M563554  L037470,442449,453460,46404010400007     
M|54|54A564554B541ZB471B5061|0 B534    L035505,478485,490494,50204010400008     
MD3ND3NMD3ND3NMD3ND3NMD3ND3NMD3ND3N    L035540,513520,527534,04004010400009     
MD3ND3NB411   0018004001               L024564,548552,555558,56156410400010     
                                       /400080                         0011     

FOOTNOTES

[1] By convention, columns 1-5 are for PGLIN, that is, the page and line number on the coding sheet. In real life, I would have punched these, but I got lazy since they are optional and I was really working with a text file, so there was not real danger of dropping the source code and being unable to run the deck through a sorter. The actual source code occupies columns 6-72, while columns 73-80 are often used (in an older convention) for similar sequencing purposes.

[2] The 1401 uses three areas of memory for basic I/O. 1-80 are the Card Read buffer, 101-180 are the Card Punch buffer, and 201-332 are the Print Buffer. Those were the only I/O devices in the original design. Later models added Tape, Disk, and a sort of generic I/O option. The follow-on 1440 used that option for all I/O, but the same areas were typically used by convention to ease portability. In this case, the Punch Buffer is available for general use since we are not punching cards.

[3] An accountant who dropped a full box of cards on his foot might say "&#@$%" while an engineer might say "+='$(".

I originally thought I'd have this program read a card, translate it to Morse Code, and send that. Then I looked at the size of the needed table, the difficulty of doing a table-lookup on a character, and the fact that I'd be punching all this by hand, and decided to "Bake the message in"

[4] out of the available choices of 1400, 4000, 8000, 12000, or 16000. Since both the 1401s I have access to have 16K, I'll live with that. This program also relies on having the Advanced Programming option, which includes subroutines and index registers. Yes, lots of stuff was optional, and 1401s were typically built to order. One of the CHM machines has the Sterling Currency option, proving that computing and LSD go way back.

[5] Addresses on the 1401 are 3 character long decimal numbers. The normal addition operation modifies the Zone Bits on the most significant digit on the case of overflow, changing that digit to a letter or symbol. For example, the highest address on the base (1400 character) model was T99. When models larger than 4K were produced (requiring an external cabinet for the other 1-3 4K stacks), the zone bits over the least significant digit were used, but this was no longer a side effect of regular addition, so the MA instruction was added. Indexing used the zone bits of the middle digit.

[6] The assembler provides Literals, which are anonymous initialized variables (that should always be used as constants, but you know programmers) whose address is filled in to the instruction using them. e.g.

     CLOOP     MA   XINC,X1
could have been written
     CLOOP     MA   @001@,X1
but I like to name all but the most trivial constants. Besides, if I used a literal, it could be Pooled with other uses so I couldn't patch it without borking other uses.

[7] In retrospect, it sounds really slow, but since the tone frequency and the timing of the tone and silence are all interdependent, and I was running out of time, we left it that way.

Future Direction: Take tone frequency and speed as inputs, tweak all relevant locations, e.g. the "constants" ELINC, DITCNT, DAHCNT, and all uses of the symbols TADDR and SADDR.

[8] Note the loop bottom code. We can branch if an overflow happened, but there is no corresponding "Branch on NO Arithmetic Overflow", so we have to branch around the looping branch. Most conditional branches on the 1401 have only one sense, a fact I don't recall noticing at the time. Later, having used machines that could branch on EQual OR Not Equal, I noticed.